package net.andreinc.jet.xml;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;


import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public final class XMLElementTree {
	/**
	 * Internal usage class . Association between an Element object , it's
	 * parent Element and a DOM Node .
	 * 
	 * @author
	 * 
	 */
	private class ElementNodeAssoc {
		public XMLElement parent = null;
		public XMLElement element = null;
		public Node node = null;

		public ElementNodeAssoc(XMLElement element, XMLElement parent, Node node) {
			this.element = element;
			this.node = node;
			this.parent = parent;
		}
	}

	private XMLElement root = null;

	/**
	 * Creates an element ElementTree object from an existing root element .
	 * 
	 * @param root
	 *            The root element
	 */
	public XMLElementTree(XMLElement root) {
		this.root = root;
	}

	/**
	 * Creates an ElemenTree object from an existing XML file object .
	 * 
	 * @param file
	 *            The XML file oject .
	 * @throws FileNotFoundException
	 * @throws IOException
	 * @throws SAXException
	 *             If the file is invalid .
	 */
	public XMLElementTree(File file) throws FileNotFoundException, IOException, SAXException {
		InputStream inStream = new FileInputStream(file);
		initFromStream(inStream);
		inStream.close();
	}

	/**
	 * Creates an ElementTree object from an existing XML stream
	 * 
	 * @param inStream
	 *            The XML input stream .
	 * @throws IOException
	 * @throws SAXException
	 */
	public XMLElementTree(InputStream inStream) throws IOException, SAXException {
		initFromStream(inStream);
	}

	/**
	 * Reads an XML input stream and initialize the root Element .
	 * 
	 * @param inStream
	 *            The XML input stream .
	 * @throws IOException .
	 * @throws SAXException
	 *             If the XML stream is invalid .
	 */
	private void initFromStream(InputStream inStream) throws IOException, SAXException {
		Document doc = null;
		DocumentBuilderFactory dbf = null;
		DocumentBuilder docBuild = null;
		try {
			Stack<ElementNodeAssoc> stack = null;
			ElementNodeAssoc head = null, ena = null;
			NodeList nodeList = null;

			dbf = DocumentBuilderFactory.newInstance();
			docBuild = dbf.newDocumentBuilder();

			doc = docBuild.parse(inStream);

			// Push the root node onto the stack
			stack = new Stack<ElementNodeAssoc>();
			stack.push(new ElementNodeAssoc(new XMLElement(), null, doc.getFirstChild()));
			initiateElement(stack.peek());

			// The root element is current head of the newly created stack
			this.root = stack.peek().element;

			while (!stack.isEmpty()) {
				head = stack.pop();
				nodeList = head.node.getChildNodes();
				for (int i = 0; i < nodeList.getLength(); ++i) {
					if (nodeList.item(i).getNodeType() == Node.ELEMENT_NODE) {
						ena = new ElementNodeAssoc(new XMLElement(), head.element, nodeList.item(i));
						initiateElement(ena);
						head.element.addChildren(ena.element);
						stack.push(ena);
					}
				}
			}
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Function used internally to initiate text, tag and attributes values for
	 * a specific node . The subElements is initiated with new empty linkedList
	 * .
	 * 
	 * @param element
	 * @param node
	 */
	private void initiateElement(XMLElement element, XMLElement parent, Node node) {
		String nodeText = null, nodeTag = null;
		Map<String, String> attributes = null;
		List<XMLElement> subElements = null;
		NamedNodeMap nnm = null;
		Attr attr = null;

		// Init Element components
		nodeText = node.getTextContent();
		nodeTag = node.getNodeName();

		// Populate attributes
		attributes = new LinkedHashMap<String, String>();
		nnm = node.getAttributes();
		for (int i = 0; i < nnm.getLength(); ++i) {
			if (nnm.item(i).getNodeType() == Node.ATTRIBUTE_NODE) {
				attr = (Attr) nnm.item(i);
				attributes.put(attr.getName(), attr.getValue());
			}
		}

		subElements = new LinkedList<XMLElement>();

		// Initiate element
		element.setTag(nodeTag);
		element.setText(nodeText);
		element.setAttributes(attributes);
		element.addChildren(subElements);
		element.setParent(parent);
	}

	/**
	 * Function used internally to initiate text, tag and attributes values for
	 * a specific node . The subElements is initiated with new empty linkedList
	 * .
	 * 
	 * @param ena
	 */
	private void initiateElement(ElementNodeAssoc ena) {
		initiateElement(ena.element, ena.parent, ena.node);
	}

	/**
	 * Returns the root element from the ElementTree
	 * 
	 * @return
	 */
	public XMLElement getRoot() {
		return root;
	}

	/**
	 * Updates the root element from the ElementTree . Use it with care .
	 * 
	 * @param root
	 */
	public void setRoot(XMLElement root) {
		this.root = root;
	}

	/**
	 * Writes the current implementation of ElementTree to a file .
	 * 
	 * @param filePath
	 */
	public void write(String filePath) {
		throw new RuntimeException("Not implemented");
	}

	public static void main(String args[]) throws FileNotFoundException, IOException, SAXException {
		XMLElementTree t = new XMLElementTree(new File("test.xml"));
		XMLElement root = t.getRoot();
		for (Iterator<XMLElement> it = root.getAllChildren(); it.hasNext();) {
			System.out.println(it.next().getTag());
		}
	}
}